using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using ServiceStack.Auth;
using ServiceStack.Serialization;
using ServiceStack.Text;
using ServiceStack.Web;

namespace ServiceStack.Formats;

public class HtmlFormat : IPlugin, Model.IHasStringId
{
    public string Id { get; set; } = Plugins.Html;
    public static string TitleFormat
        = @"{0} Snapshot of {1}";

    public static string HtmlTitleFormat
        = @"Snapshot of <i>{0}</i> generated by <a href=""https://servicestack.net"">ServiceStack</a> on <b>{1}</b>";

    public static bool Humanize = true;

    private IAppHost AppHost { get; set; }
        
    public Dictionary<string, string> PathTemplates { get; set; } = new() {
        { "/" + LocalizedStrings.Auth.Localize(), "/Templates/auth.html" }
    };
        
    public Func<IRequest, string> ResolveTemplate { get; set; }

    public string DefaultResolveTemplate(IRequest req)
    {
        if (PathTemplates != null && PathTemplates.TryGetValue(req.PathInfo, out var templatePath))
        {
            var file = HostContext.VirtualFileSources.GetFile(templatePath);
            if (file == null)
                throw new FileNotFoundException($"Could not load HTML template '{templatePath}'", templatePath);

            return file.ReadAllText();
        }
        return null;
    }

    public HtmlFormat()
    {
        ResolveTemplate = DefaultResolveTemplate;
    }

    public void Register(IAppHost appHost)
    {
        AppHost = appHost;
        //Register this in ServiceStack with the custom formats
        appHost.ContentTypes.RegisterAsync(MimeTypes.Html, SerializeToStreamAsync, null);

        appHost.Config.DefaultContentType = MimeTypes.Html;
        appHost.Config.IgnoreFormatsInMetadata.Add(MimeTypes.Html.ToContentFormat());
    }

    public async Task SerializeToStreamAsync(IRequest req, object response, Stream outputStream)
    {
        var res = req.Response;
        if (req.GetItem("HttpResult") is IHttpResult httpResult && httpResult.Headers.ContainsKey(HttpHeaders.Location) 
                                                                && httpResult.StatusCode != System.Net.HttpStatusCode.Created)  
            return;

        try
        {
            if (res.StatusCode >= 400)
            {
                var responseStatus = response.GetResponseStatus();
                req.SetItem(Keywords.ErrorStatus, responseStatus);
            }

            if (response is CompressedResult)
            {
                if (res.Dto != null)
                    response = res.Dto;
                else 
                    throw new ArgumentException("Cannot use Cached Result as ViewModel");
            }

            foreach (var viewEngine in AppHost.ViewEngines)
            {
                var handled = await viewEngine.ProcessRequestAsync(req, response, outputStream);
                if (handled)
                    return;
            }
        }
        catch (Exception ex)
        {
            if (res.StatusCode < 400)
                throw;

            //If there was an exception trying to render a Error with a View, 
            //It can't handle errors so just write it out here.
            response = DtoUtils.CreateErrorResponse(req.Dto, ex);
        }

        //Handle Exceptions returning string
        if (req.ResponseContentType == MimeTypes.PlainText)
        {
            req.ResponseContentType = MimeTypes.Html;
            res.ContentType = MimeTypes.Html;
        }

        if (req.ResponseContentType != MimeTypes.Html && req.ResponseContentType != MimeTypes.JsonReport) 
            return;

        var dto = response.GetDto();
        if (!(dto is string html))
        {
            // Serialize then escape any potential script tags to avoid XSS when displaying as HTML
            var json = JsonDataContractSerializer.Instance.SerializeToString(dto) ?? "null";
            json = json.HtmlEncodeLite();

            var url = req.ResolveAbsoluteUrl()
                .Replace("format=html", "")
                .Replace("format=shtm", "")
                .TrimEnd('?', '&')
                .HtmlEncode();

            url += url.Contains("?") ? "&" : "?";

            var now = DateTime.UtcNow;
            var requestName = req.OperationName ?? dto.GetType().GetOperationName();
            var serverInfo = new ServerInfo
            {
                JsonApiRoute = AppHost.GetPlugin<PredefinedRoutesFeature>()?.JsonApiRoute
            };

            html = ReplaceTokens(ResolveTemplate?.Invoke(req) ?? Templates.HtmlTemplates.GetHtmlFormatTemplate(), req)
                    .Replace("${RequestName}", EncodeForJavaScriptString(requestName))
                    .Replace("${ServerInfo}", serverInfo.ToJson(configure:x => x.TextCase = Text.TextCase.CamelCase))
                    .Replace("${RequestDto}", JsonDataContractSerializer.Instance.SerializeToString(req.Dto)?.HtmlEncodeLite() ?? "null")
                    .Replace("${Dto}", json)
                    .Replace("${Title}", string.Format(TitleFormat, requestName, now))
                    .Replace("${MvcIncludes}", MiniProfiler.Profiler.RenderIncludes().ToString())
                    .Replace("${Header}", string.Format(HtmlTitleFormat, requestName, now))
                    .Replace("${ServiceUrl}", EncodeForJavaScriptString(url))
                    .Replace("${Humanize}", Humanize.ToString().ToLower())
                ;
        }
            
        await ((ServiceStackHost)AppHost).WriteAutoHtmlResponseAsync(req, response, html, outputStream);
    }

    public static string ReplaceTokens(string html, IRequest req)
    {
        if (string.IsNullOrEmpty(html))
            return string.Empty;

        html = html
                .Replace("${BaseUrl}", EncodeForJavaScriptString(req.GetBaseUrl().WithTrailingSlash()))
                .Replace("${AuthRedirect}",  EncodeForJavaScriptString(req.ResolveAbsoluteUrl(HostContext.AppHost.GetPlugin<AuthFeature>()?.HtmlRedirect)))
                .Replace("${AllowOrigins}", EncodeForJavaScriptString(HostContext.AppHost.GetPlugin<CorsFeature>()?.AllowOriginWhitelist?.Join(";")))
                .Replace("${NoProfileImgUrl}", EncodeForJavaScriptString(req.TryResolve<IAuthMetadataProvider>()?.GetProfileUrl(null)) ?? JwtClaimTypes.DefaultProfileUrl)
            ;
        return html;
    }
    
    /// <summary>
    /// Encodes a string so it can be safely embedded within double-quoted JavaScript strings.
    /// Handles escape sequences, Unicode characters, and line breaks.
    /// </summary>
    public static string EncodeForJavaScriptString(string input)
    {
        if (input == null)
            return "";
        
        if (input.Length == 0)
            return string.Empty;
        
        var sb = StringBuilderCache.Allocate();
        foreach (char c in input)
        {
            switch (c)
            {
                case '"':
                    sb.Append("\\\"");
                    break;
                case '\\':
                    sb.Append("\\\\");
                    break;
                case '\b':
                    sb.Append("\\b");
                    break;
                case '\f':
                    sb.Append("\\f");
                    break;
                case '\n':
                    sb.Append("\\n");
                    break;
                case '\r':
                    sb.Append("\\r");
                    break;
                case '\t':
                    sb.Append("\\t");
                    break;
                case '\v':
                    sb.Append("\\v");
                    break;
                case '\0':
                    sb.Append("\\0");
                    break;
                default:
                    // Handle control characters and non-ASCII characters
                    if (c < 32 || c > 126)
                    {
                        sb.AppendFormat("\\u{0:x4}", (int)c);
                    }
                    else
                    {
                        sb.Append(c);
                    }
                    break;
            }
        }
        return StringBuilderCache.ReturnAndFree(sb);
    }    
}

public class ServerInfo
{
    public string JsonApiRoute { get; set; }
}